#region "Copyright (C) Lenny Granovsky. 2005-2013"
//This program is free software distribute under the terms of the GNU General Public License as published by
//the Free Software Foundation. Please read AssemblyInfo.cs file for more information.
#endregion

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using System.Web;
using System.Web.Services;
using System.Reflection;
using System.IO;
using System.Web.Caching;
using System.Web.Services.Protocols;
using System.Web.UI;

namespace Web.Enhancements.Rest
{
    [Serializable()]
    internal class WebRestHandler : IHttpHandler
    {
        private string DefaultContentType = "text/html";

        public WebRestHandler()
        {
        }

        public void ProcessRequest(HttpContext context)
        {
#if ENABLE_TIMING
            System.Diagnostics.Stopwatch watch = null;
#endif
            try
            {
#if ENABLE_TIMING
                watch = (System.Diagnostics.Stopwatch)context.Items["WebRestMainTimer"];
                System.Diagnostics.Trace.WriteLine(string.Format("WebRest handler started - {0} ticks elapsed.", watch.ElapsedTicks.ToString()));
#endif
                object target = context.Items["WebRestHanlerInvocationTarget"];
                context.Items.Remove("WebRestHanlerInvocationTarget");
                LogicalMethodInfo methodInfo = WebRestServiceContext.Current.MethodInfo;
                if (this.CheckCancellation(target))
                    return;

                //resolve method parameters
                object[] objs = ReadParameters(context.Request, methodInfo, WebRestServiceContext.Current.MethodInfoCache.HttpBodyParameterInfo);
                WebRestServiceContext.Current.MethodResolvedParameters = objs;

                //call Filters before method call
                foreach (Filters.IWebRestFilter filter in WebRestServiceContext.Current.Filters)
                {
#if TRACE
                    System.Diagnostics.Trace.WriteLine(string.Format("Calling {0} REST filter before the actual call.", filter.GetType().FullName));
#endif
                    filter.BeforeMethodCall(context, WebRestServiceContext.Current);
                    if (this.CheckCancellation(target))
                        return;
                }

                //call actual method
                object data = methodInfo.MethodInfo.Invoke(target, objs);
                if (this.CheckCancellation(target))
                    return;

                //call Filters after method call
                foreach (Filters.IWebRestFilter filter in WebRestServiceContext.Current.Filters)
                {
#if TRACE
                    System.Diagnostics.Trace.WriteLine(string.Format("Calling {0} REST filter after the actual call.", filter.GetType().FullName));
#endif
                    filter.AfterMethodCall(context, ref data);
                    if (this.CheckCancellation(target))
                        return;
                }

                //set content type
                string contentType = DefaultContentType;
                if (WebRestServiceContext.Current.ContentType.Length > 0)
                    contentType = WebRestServiceContext.Current.ContentType;

                //set cacheability depending on configuration
                (new OutputCacheInitializer()).InitOutputCache(WebRestServiceContext.Current.MethodInfoCache.CacheParameters);
                context.Response.ContentType = contentType;

                if (data != null)
                {
                    if (data.GetType().GetInterface("Web.Enhancements.Rest.Templates.IStaticFileResult") != null) //static file
                    {   //handle Static file
                        Web.Enhancements.Rest.Templates.IStaticFileResult result1 = (Templates.IStaticFileResult)data;
                        result1.RenderResult(context.Response);
                    }
                    else
                    {
                        if (WebRestServiceContext.Current.ContentDisposition.Length > 0)
                            context.Response.AppendHeader("Content-Disposition", WebRestServiceContext.Current.ContentDisposition);

                        string dataOutputType = data.GetType().FullName.ToLower();
                        if (data.GetType().GetInterface("Web.Enhancements.Rest.Templates.ITemplatedResult") != null) //HTML template
                        {   //handle HTML template
                            Web.Enhancements.Rest.Templates.ITemplatedResult result = (Templates.ITemplatedResult)data;
                            result.RenderResult(context.Response);
                        }
                        else if (contentType.ToUpper().Contains("JSON") && dataOutputType != "system.string") //convert it to JSON
                        {
                            Templates.ITemplatedResult json = null;
                            if (WebRestServiceContext.Current.JsonSerializer == JsonSerializerType.JavaScriptSerializer)
                                json = new Templates.JsonTemplate(data);
                            else if (WebRestServiceContext.Current.JsonSerializer == JsonSerializerType.CustomJsonSerializer)
                                json = new Templates.CustomJsonTemplate(data);
                            else
                                json = new Templates.DataContractJsonTemplate(data);
                            json.RenderResult(context.Response);
                        }
                        else if (contentType.ToUpper().Contains("XML") && dataOutputType != "system.string") //convert it to XML
                        {
                            Templates.ITemplatedResult xml = new Templates.XmlTemplate(data);
                            xml.RenderResult(context.Response);
                        }
                        else if (dataOutputType == "system.byte[]") //handle binary data
                            context.Response.BinaryWrite((byte[])data);
                        else
                            context.Response.Write(data.ToString()); //handle string data
                    }
                }

                //dispose object, if requires
                DisposeObject(target);
            }
            catch(Filters.WebRestSecurityException eSec)
            {
#if TRACE
                System.Diagnostics.Trace.WriteLine(string.Format("WebRest handler thrown an exception. Error: {0}", eSec.ToString()));
#endif
                if (WebRestSettings.Current.IncludeExceptionDetailInErrors)
                    throw;
                else
                {
                    context.Response.StatusCode = 401;
                    context.Response.StatusDescription = "The requested resource requires user authentication.";
                    context.Response.Flush();
                    context.ApplicationInstance.CompleteRequest();
                }
            }
            catch (System.Threading.ThreadAbortException)
            {
                throw;
            }
            catch (Exception e)
            {
#if TRACE
                System.Diagnostics.Trace.WriteLine(string.Format("WebRest handler thrown an exception. Error: {0}", e.ToString()));
#endif
                if (WebRestSettings.Current.IncludeExceptionDetailInErrors)
                {
                    throw;
                }
                else
                {
                    context.Response.StatusCode = 500;
                    context.Response.StatusDescription = "The server encountered an unexpected condition that prevented it from fulfilling the request.";
                    context.Response.Flush();
                    context.ApplicationInstance.CompleteRequest();
                }
            }
            finally
            {
#if ENABLE_TIMING
                System.Diagnostics.Trace.WriteLine(string.Format("WebRest handler ended - {0} ticks elapsed ({1} ms).", watch.ElapsedTicks.ToString(), watch.ElapsedMilliseconds.ToString()));
                watch.Stop();
                System.Diagnostics.Trace.WriteLine("");
#endif
            }
        }

        private bool CheckCancellation(object target)
        {
            if (WebRestServiceContext.Current.Cancel)
            {   //process cancellation
                DisposeObject(target);
            }
            return WebRestServiceContext.Current.Cancel;
        }

        private static void DisposeObject(object target)
        {
            if (target.GetType().GetInterface("System.IDisposable") != null)
                ((System.IDisposable)target).Dispose();
        }

        public bool IsReusable
        {
            get { return true; }
        }

        /// <summary>
        /// Method reads all parameters required by rest service method from current request.
        /// </summary>
        /// <param name="request">Current HttpRequest object.</param>
        /// <param name="methodInfo">Method info.</param>
        /// <param name="bodyParameterInfo">An additional information when parameter object should be build from the request input stream.</param>
        /// <returns>Returns an array of object to serve as parameters when REST method gets called.</returns>
        internal object[] ReadParameters(HttpRequest request, System.Web.Services.Protocols.LogicalMethodInfo methodInfo, HttpBodyParameterInstructions bodyParameterInfo)
        {
            NameValueCollection requestedParams = new NameValueCollection(request.QueryString);
            if (HttpContext.Current.Items.Contains("WebRestRoutedQueryStringParams"))
            {
                NameValueCollection routedQS = (NameValueCollection)HttpContext.Current.Items["WebRestRoutedQueryStringParams"];
                foreach (string key in routedQS.AllKeys)
                {
                    if (requestedParams[key] == null)
                        requestedParams.Add(key, routedQS[key]);
                }
            }

            //build http body param context
            HttpBodyParameterContext ppc = new HttpBodyParameterContext() { 
                ParameterName = bodyParameterInfo.ParameterName, 
                ConversionHint = bodyParameterInfo.ConversionHint,
                UseCustomJsonDeserialiser = bodyParameterInfo.UseCustomJsonDeserialiser,
                InputStream = request.InputStream,
                ContentType= request.ContentType
            };
            MimeParameterReader rdr = null;
            if (request.HttpMethod != "POST" && request.HttpMethod != "PUT" && request.HttpMethod != "UPDATE")
                rdr = (MimeParameterReader)UrlParameterReader.CreateInstance(typeof(UrlParameterReader), UrlParameterReader.GetInitializer(typeof(UrlParameterReader), methodInfo, ppc.ParameterName));
            else
            {
                foreach (string key in request.Form.AllKeys)
                {
                    if (requestedParams[key] == null)
                        requestedParams.Add(key, request.Form[key]);
                }
                rdr = (MimeParameterReader)HtmlFormParameterReader.CreateInstance(typeof(HtmlFormParameterReader), HtmlFormParameterReader.GetInitializer(typeof(HtmlFormParameterReader), methodInfo, ppc.ParameterName));
            }
            object[] arr = rdr.Read(requestedParams, ppc);
            if (arr != null)
                return arr;
            return new object[0];
        }
    }

    [Serializable()]
    internal class WebRestSessionReadOnlyHandler : WebRestHandler, System.Web.SessionState.IReadOnlySessionState
    {
        public WebRestSessionReadOnlyHandler()
        {
        }
    }

    [Serializable()]
    internal class WebRestSessionEnabledHandler : WebRestHandler, System.Web.SessionState.IRequiresSessionState
    {
        public WebRestSessionEnabledHandler()
        {
        }
    }
}
